package com.ljx.funchat.websocket.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ljx.funchat.common.event.UserOfflineEvent;
import com.ljx.funchat.common.event.UserOnlineEvent;
import com.ljx.funchat.user.dao.UserDao;
import com.ljx.funchat.user.domain.entity.User;
import com.ljx.funchat.user.service.LoginService;
import com.ljx.funchat.websocket.NettyUtil;
import com.ljx.funchat.websocket.domain.dto.WSChannelExtraDTO;
import com.ljx.funchat.websocket.domain.vo.resp.WSBaseResp;
import com.ljx.funchat.websocket.service.WebSocketService;
import com.ljx.funchat.websocket.service.adatper.WebSocketAdapter;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 专门管理websocket逻辑，包括推拉
 */
@Slf4j
@Service
public class WebSocketServiceImpl implements WebSocketService {

    // 管理所有用户channel和uuid的对应关系【包括游客】
    private static final ConcurrentHashMap<Channel, WSChannelExtraDTO> ONLINE_WS_MAP = new ConcurrentHashMap<>();

    // 所有在线的用户和对应的socket
    private static final ConcurrentHashMap<Long, CopyOnWriteArrayList<Channel>> ONLINE_UID_MAP = new ConcurrentHashMap<>();


    public static final int MAXIMUM_SIZE = 1000;
    // 持续时间为1小时
    public static final Duration DURATION = Duration.ofHours(1);
    // Caffeine：既可以并发，又有过期时间，又有容量
    // 临时保存code和channel的映射关系
    private static final Cache<Integer, Channel> WAIT_LOGIN_MAP = Caffeine.newBuilder()
            .maximumSize(MAXIMUM_SIZE) // 设置最大容量
            .expireAfterWrite(DURATION) // 设置有效时间为1小时
            .build();

    @Autowired
    @Lazy
    private WxMpService wxMpService;

    @Autowired
    private UserDao userDao;

    @Autowired
    private LoginService loginService;

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    /**
     * 保存用户连接
     * @param channel
     */
    @Override
    public void connect(Channel channel) {
        ONLINE_WS_MAP.put(channel, new WSChannelExtraDTO());
    }

    /**
     * 获取登录请求二维码
     * @param channel
     */
    @Override
    public void handleLoginReq(Channel channel) {
        // 生成登录随机码
        Integer code = generateLoginCode(channel);
        // 微信生成带参二维码
        WxMpQrCodeTicket wxMpQrCodeTicket = null;
        try {
            wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, (int) DURATION.getSeconds());
        } catch (WxErrorException e) {
            e.printStackTrace();
        }
        // 把二维码推送前端
        sendMsg(channel, WebSocketAdapter.buildResp(wxMpQrCodeTicket));
    }

    /**
     * 把二维码推送前端
     * @param channel
     * @param wsBaseResp
     */
    private void sendMsg(Channel channel, WSBaseResp<?> wsBaseResp) {
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(wsBaseResp)));
    }

    /**
     * 生成登录随机码【不重复】
     * 如果所指定的 key 已经在 HashMap 中存在，返回和这个 key 值对应的 value, 如果所指定的 key 不在 HashMap 中存在，则返回 null
     * @param channel
     * @return
     */
    private Integer generateLoginCode(Channel channel) {
        Integer code;
        do {
            code = RandomUtil.randomInt(Integer.MAX_VALUE);
        } while (Objects.nonNull(WAIT_LOGIN_MAP.asMap().putIfAbsent(code, channel)));
        return code;
    }

    /**
     * 移除用户连接
     * @param channel
     */
    @Override
    public void offline(Channel channel) {
        // 获取uid
        WSChannelExtraDTO wsChannelExtraDTO = ONLINE_WS_MAP.get(channel);
        Optional<Long> uid = Optional.ofNullable(wsChannelExtraDTO)
                .map(WSChannelExtraDTO::getUid);
        boolean isOffline = isOffLine(uid, channel);
        if (isOffline && uid.isPresent()){
            //已登录用户断连,并且全下线成功
            User user = new User();
            user.setId(uid.get());
            user.setLastOptTime(new Date());
            applicationEventPublisher.publishEvent(new UserOfflineEvent(this, user));
        }
    }

    /**
     * 判断用户是否下线，没下线就下线
     * @param uid
     * @return
     */
    private boolean isOffLine(Optional<Long> uid, Channel channel) {
        // 去除连接Map
        ONLINE_WS_MAP.remove(channel);
        if (uid.isPresent()){
            // 去掉在线用户Map
            CopyOnWriteArrayList<Channel> channels = ONLINE_UID_MAP.get(uid.get());
            if (CollectionUtil.isNotEmpty(channels)) {
                channels.removeIf(ch -> Objects.equals(ch, channel));
            }
            return CollectionUtil.isEmpty(ONLINE_UID_MAP.get(uid.get()));
        }
        return true;
    }

    @Override
    public void scanLoginSuccess(Integer code, Long id) {
        Channel channel = WAIT_LOGIN_MAP.getIfPresent(code);
        if (ObjectUtil.isNull(channel)){
            return;
        }
        // 再次查询用户信息【确保最新】
        User user = userDao.getById(id);
        // 移除code
        WAIT_LOGIN_MAP.invalidate(code);
        // 获取token
        String token = loginService.login(id);
        loginSuccess(channel, user, token);
    }

    private void loginSuccess(Channel channel, User user, String token) {
        online(channel, user.getId());
        // 给前端推送消息
        sendMsg(channel, WebSocketAdapter.buildResp(user, token));
        // 用户上线事件
        user.setLastOptTime(new Date());
//        user.refreshIp(NettyUtil.getAttr(channel, NettyUtil.IP));
        applicationEventPublisher.publishEvent(new UserOnlineEvent(this, user));
    }

    /**
     * 用户上线
     * @param channel
     * @param uid
     */
    private void online(Channel channel, Long uid) {
        // 保存用户channel和uid对应关系
        // 当用户一连接就会有个对应的channel
        ONLINE_WS_MAP.get(channel).setUid(uid);
        // 保存用户上线
        ONLINE_UID_MAP.putIfAbsent(uid, new CopyOnWriteArrayList<>());
        ONLINE_UID_MAP.get(uid).add(channel);

    }

    @Override
    public void waitAuthorize(Integer code) {
        Channel channel = WAIT_LOGIN_MAP.getIfPresent(code);
        if (ObjectUtil.isNull(channel)){
            return;
        }
        sendMsg(channel, WebSocketAdapter.buildWaitAuthorizeResp());
    }

    @Override
    public void authorize(Channel channel, String token) {
        Long uid = loginService.getValidUid(token);
        if (Objects.nonNull(uid)){
            // 存在
            User user = userDao.getById(uid);
            loginSuccess(channel, user, token);
        } else {
            // 不存在
            // 给前端推送消息
            sendMsg(channel, WebSocketAdapter.buildInvalidTokenResp());
        }
    }

    /**
     * 发送消息【个别用户】
     * @param uid
     * @param buildMsgSend
     */
    @Override
    public void sendToUid(Long uid, WSBaseResp<?> buildMsgSend) {
        CopyOnWriteArrayList<Channel> channels = ONLINE_UID_MAP.get(uid);
        if (CollectionUtil.isEmpty(channels)) {
            log.info("用户：{}不在线", uid);
            return;
        }
        channels.forEach(channel -> {
            threadPoolTaskExecutor.execute(() -> sendMsg(channel, buildMsgSend));
        });
    }

    @Override
    public void sendToAllOnline(WSBaseResp<?> resp) {
        ONLINE_WS_MAP.forEach((channel, ext) -> {
//            if (Objects.nonNull(skipUid) && Objects.equals(ext.getUid(), skipUid)) {
//                return;
//            }
            threadPoolTaskExecutor.execute(() -> sendMsg(channel, resp));
        });
    }
}
